论坛首页 Java企业应用论坛

基础知识: 需求!

浏览 109742 次
该帖已经被评为精华帖
作者 正文
   发表时间:2004-08-20  
我就不明白, 一个简简单单的需求分析, 在经过了这么多专家的仔细阐述分析之后,怎么让大家搞得越来越复杂了?

什么是面向接口? 就是你分析了需求, 根据需求定义了接口, 然后所有东西都围着这个接口转.

有人可能觉得一个类通过重构变得那样太不可接受了. 那么我请问, 你的重构的底线是什么? 类变成什么样你可以接受? 你那么关心类的实现方法干什么? 你到底是面向接口编程还是面向类编程?

一个模块X, 它要实现接口I, 然后要让别人用自己的实例.
仔细分析, 这里面的需求是什么?

只有一个
1. X要给外界提供一个实现了I接口的实例.

没有别的了. 真的, 没有别的了. 我只要能保证调用者从我这里取得一个I的实例, 我就完成了工作. 你管我是偷来的抢来的?

这个需求要求你必须用X实现I了么?
它要求你必须每次调用的时候返回不同的实现了么?

它告诉你X不能开始时候实现I, 后来又不实现了么?

同志们, 放下骄傲的直觉, 科学理性地分析一下吧.

通过数学的方法, 从需求1, 你得不出上述得任何结论!

根据这个需求,

class X implements I{
  public X();{}
}

符合要求.

class X implements I{
  public static I instance();{return new X();;}
}

符合要求

class X{
  static class Inner implements I{
  }
  public static I instance();{return new Inner();;}
}


符合要求.

class X{
  public static I instance();{return GenericI.instance();;}
}

符合要求

class X{
  class Inner implements I{
  }
  private static final X singleton = new X();;
  public static I instance();{return singleton.new Inner();;}
}


仍然符合要求.

设计是什么? 就是在分析了需求之后, 保留真正的需求, 剔除一些假想的并不真正存在的假设.

什么外部用容器啦, 什么类X不能从implements I变成不implements I啦, 都是你自己的一厢情愿的假设.

如果分析出了这个需求, 谁是最能够忠实反映这个需求而添加的额外约束最小的? 构造函数还是静态工厂?

一个假设最小的接口就是最灵活的接口. 什么外部创建逻辑, 什么容器, 这些外部的逻辑没有理由在一个不灵活的接口上可以做到而在一个更灵活的接口上反而做不到.

大家认真想想.
   发表时间:2004-08-20  
ajoo抛出来的这个问题本质上还是他一遍又一遍不厌其烦要证明的静态工厂比公共构造函数好的老问题上来了。

这个问题ajoo在下面这个帖子开宗明义指出静态工厂好,构造函数不好的若干理由

http://forum.iteye.com/viewtopic.php?t=6836

虽然后来东扯西拉了很多IoC容器拉,配置文件拉什么的,拉拉杂杂吵了好几个主题总共几百贴,不过本质问题还是在于" 静态工厂说 vs 公共构造函数说"。

之后Charon,Readonly等人的反击也没有击中要害,charon的抽象工厂说,readonly的容器配置说还是攻击枝叶,不及其根。所以让我们正本清源,比较一下双方的论点和论据吧:

挑战者(正方):ajoo
立场:静态工厂说
观点:静态工厂比公共构造函数更灵活
主要阐述:
http://forum.iteye.com/viewtopic.php?t=6836
(见主题贴发言内容)

应战者(反方):potian, charon, readonly以及若干人等
立场:公共构造函数说
观点:公共构造函数不假设对象的创建行为,适用于各种场合
主要阐述:
http://forum.iteye.com/viewtopic.php?t=6904
(见potian发言)


双方论点差异比较:

1、谁来创建对象

公共构造函数说:
把对象的创建职责交给外部的调用者(容器,组装代码,配置文件,etc)来完成

静态工厂说:
把对象的创建职责封闭起来,由对象自己完成


2、对象的职责是什么?

公共构造函数说:
对象的职责是描述对象所代表的业务逻辑。业务逻辑是对象重用的基础,是改变频率很低的部分,业务逻辑高于一切,是对象的核心价值。

静态工厂说:
对象的职责是完成接口契约定义的语义,接口契约高于一切,是对象的核心价值。


3、对象设计遵循的准则

公共构造函数说:
将对象创建的方式,方法和时机全部开放出去,信任外部的对象创建者,来保证对象内部业务逻辑的相对稳定性,来提高不同场合的对象重用度。

静态工厂说:
将对象创建的方式,方法和时机完全封闭起来,拒绝信任外部的对象创建者,来保证对象接口语义的最大稳定性。

4、效果预测(这是我根据双方论点的推断)

公共构造函数说:
保证了对象代表的业务逻辑稳定性,从而使对象可以从容的被重用在各种不同的场合。但是苦了外部的对象组装者,外部组装代码可能需要比较频繁的修改(由于IoC和配置文件的存在,使得这部分工作得以减轻)

静态工厂说:
保证了对象接口语义的稳定性,从而使外部组装代码可以不需要任何修改,甚至对象本身业务逻辑发生重大改变(例如ajoo的那个例子中TerseHelloImpl的业务逻辑由sayHello变成了Comparator这的比较器)也在所不惜。

从以上的比较,我认为:

5、出发点(或者说要达到的目标):

公共构造函数说:
业务逻辑对象的稳定性高于组装代码的稳定性。在保证业务逻辑最大稳定性和最大可重用度的目标下牺牲组装代码的稳定性,必要的时候可以修改组装代码。

静态工厂说:
对象接口契约稳定性高于一切,所以组装代码稳定性高于业务逻辑对象稳定性。为了保证组装代码稳定性,不惜牺牲对象所代表的业务逻辑含义,即修改业务对象的逻辑语义,来适配外部组装代码。

结语:

我尽量尝试以公正的角度来理解双方的观点,但是在比较的最后我不得不发现我开始倾向于公共构造函数说。现在流行的很多框架,例如Hibernate之类,基本上都是按照公共构造函数说的方式作为设计理念的。

ajoo可能在学术意义上比大多数人更加高明,追求的是编程的语义严谨性和逻辑的缜密性,所以他不能够容忍外部组装代码潜在的不安全调用,所以他喜欢用private和final来控制对象,只开放对象的接口契约,再无其他,接口契约是他的上帝。

但是从实际从事java软件开发的项目实践角度来说,接口契约的改变从来都不是一个致命的问题,特别是在有自动测试工具和强大的重构工具如Eclipse这样的工具支持下,接口契约的改变并不是世界末日,而只不过是家常便饭。真正的上帝是对象代表的业务逻辑的稳定性,如果业务逻辑都改变了,这才是世界末日。

拿TerseHelloImp这个例子来说,我可以不在乎组装代码从原来调用TerseHelloImp变成了GenericHello,这对于我这个组装代码编写者来说,只不过是在Eclipse里面批量替换加上重构而已。但是如果你告诉我TerseHelloImp本来是一个人见人爱的到处和人打招呼的说hello的人,现在变成了一个整天挑人说话语病的刺头,即使你告诉我,我不需要改变和他相处的方式,我也会感觉很不安,会confused的。因为我现在根本不知道这个接口语义它还代表什么含义了,我也没有办法调用了。
0 请登录后投票
   发表时间:2004-08-20  
好! 你的分析条理相当清楚, 逻辑也很缜密.

我有一点不明白的想请教一下:

在你的分析里, 把组装代码的稳定性和业务逻辑对象的稳定性作为一对矛盾提出.

那么, 这个矛盾是需要一个说明的.

有什么理由说组装代码稳定了, 业务逻辑就不稳定?

"对象所代表的业务逻辑含义"为什么在静态工厂的情况下被牺牲了?

我什么时候说要"修改业务对象逻辑来适应组装代码"了?

我举的例子里面, 那个TerseHelloImpl的外部可见行为从来没有改变过, 改变的只是内部的实现代码. hello()函数原来做什么, 改动代码后做的还是什么.  所谓"重构", 就是说程序行为一丝一毫都没有改变.
怎么说它修改了业务对象逻辑呢?


我本来的前提是: 工厂内部封装的都是和外部业务逻辑无关的实现细节. 凡是外界业务逻辑可能关心的东西都不应该被封装进工厂.

我也强调说: 工厂只封装和实现细节相关的创建逻辑, 外部仍然负责和业务逻辑相关的创建逻辑.

而我从来的逻辑都是: 组装代码也好, 容器也好, 都是要服务于业务对象的. 业务对象的实现自由不能因为容器等东西的使用而受到影响.

那么, 这里这个"矛盾说"是否需要证明一下呢?

原谅我仍然用一个比较学术化的方式来讨论问题 --- 什么东西都要证明一下再用.
potian他们这些工程师性格的人可能很不耐烦这个.
但是, 我想这是唯一能够找出分歧所在的. 即使我们讨论的是实际工作中的一些事情.
0 请登录后投票
   发表时间:2004-08-20  
不知道大家听说过重构这个词没有?

所谓重构, 就是改动现有代码, 改动结构, 而不增加功能.

看上去它和你的只增不改的信条矛盾是么?

你忘记了所谓ocp的一个前提, 它是针对需求变化的. 当需求发生了变化, 程序的外部行为需要改变了, 我们要尽量扩展而不是修改已有代码.

它和重构矛盾吗?

不矛盾. 重构, 在不改变程序行为的条件下, 修改代码, 本身也是为了在新的需求到来的时候只增不改.

这两者本来是统一的, 相辅相成的.

所以你这里把用因为重构而造成的代码修改用ocp来否定是偷换概念.

静态工厂的一个我最欣赏的作用, 就是在重构的时候可以把影响减小到最小, 局部化.

其实换一句话可以很容易解决这个ocp造的烟幕弹:
你是否从来不修改你的代码? 从来都是只增不改? 你重构过吗?

同志们, 你可能很有毅力, 不加思考地读了上千篇论文. 现在是该合起书本消化一下, 自己思考一下为什么了.
0 请登录后投票
   发表时间:2004-08-20  
其他的偶不唠叨了:
pico/spring早就支持工厂方式的, 你满意了吧?

忽然发现偶是最偏激的人: 极端反对任何静态方法. 

这几天偶的网络有问题, 难得上线, 大家继续聊, 
0 请登录后投票
   发表时间:2004-08-20  
我费了10分钟把那些无内容的纯粹吵架的回复都删除了,我不觉得单纯的吵架会有利于大多数人.
其实这个问题本没有什么问题,也就是静态工厂是不是有存在的必要的问题,进一步就是什么时候可以使用静态工厂的问题.对于ajoo说的组件和模块的区别我想大家应该引起注意,而把配置文件约束在一个合理的水平(我想究竟如何是合理的标准大家是可以讨论的,这才是有意义的)这我想不会有人会有意见.
Readonly说他:忽然发现偶是最偏激的人-- 极端反对任何静态方法. 我想静态方法是不是应该存在我们没有必要讨论,大家个人有个人的选择.你可以不用,但是别人可能会有用的场景,大家都有理由.
ajoo的问题在于总是改不了喜欢较真的毛病,大概这和他的学术研究的背景有关系.但是javaeye是一个工程化实用为主的论坛,所以也请约束一些自己的言行.
我的发言其实没有什么内容,只是为了使这个讨论可以正常进行所做的一点努力,所以我不想再对此发言.
0 请登录后投票
   发表时间:2004-08-20  
ajoo,你开的战场太多了,我已经搞不清楚要在哪里回贴了。

引用
在你的分析里, 把组装代码的稳定性和业务逻辑对象的稳定性作为一对矛盾提出.

那么, 这个矛盾是需要一个说明的.

有什么理由说组装代码稳定了, 业务逻辑就不稳定?

"对象所代表的业务逻辑含义"为什么在静态工厂的情况下被牺牲了?

我什么时候说要"修改业务对象逻辑来适应组装代码"了?

我举的例子里面, 那个TerseHelloImpl的外部可见行为从来没有改变过, 改变的只是内部的实现代码. hello()函数原来做什么, 改动代码后做的还是什么. 所谓"重构", 就是说程序行为一丝一毫都没有改变.
怎么说它修改了业务对象逻辑呢?


我解释一下我的观点。在使用类似Java这样的OO语言来设计和实施有一定规模的项目的时候,适当的分层是一个很自然的想法。这种分层从对象可重用度大小来考量,大致分为三层:

1、底层的公用框架对象
这是可重用度最高的部分,例如我的项目中我使用了Hibernate做为持久层框架实现,我使用Spring做为业务逻辑托管框架,我使用Webwork2做为表示层实现框架,blah, blah, blah,....这些可重用的对象被用在各个项目中,各个场合中,被各种组装代码所调用。当出现相应的外部需求改变的时候,你不能要求Hibernate来改变它自己的对象内部的实现,用接口契约的一致来保证你的组装调用代码不发生改变。你没有权利改变Hibernate,你只有权利改变你的调用代码,而不是去读Hibernate的sourcode,考虑改改Hibernate的sourcecode来保证你写的代码不需要被修改。

2、业务逻辑框架对象
这是可重用度比较高的部分,例如你从事的这个行业的业务框架,类似OSWorkflow之类的面向行业的业务逻辑框架。这些框架调用了Hibernate,或者说是Hibernate的外部组装者,外部创建者,好了,我现在OSWorkFlow功能要升级了,我不想改我自己OSWorkFlow的sourcecode,而是我去改Hibernate的Sourecode,用修改Hibernate内部实现代码的方式,来保证调用者的代码不需要修改。这种方式在我看来,无异于“削足适履

3、实现这个项目的组装代码
这才是我在这项目中我写出来的代码,前面两个层次的代码都不是我写的,也不需要我来维护。如果业务发生改变,你说是修改我的组装代码代价小呢?还是去修改Hibernate,OSWorkFlow的sourcecode的代价小呢?

打个比方来说,公共构造函数就是把你的脚丫光光的亮了出来,你要穿什么鞋,在什么场合穿,由鞋匠根据你的脚大小,形状和你参与社交的场合来定的,或者给你定制一双Nike运动鞋,或者给你定制一双皮鞋,blah,......你的脚是不变的,鞋可以随便换。换鞋的代价比削你的脚的代价小多了。这样做的代价就是你冒了鞋匠轻薄你的脚的风险。但是我们认为鞋匠是敬业的人,不是色狼。

但是静态工厂说认为把自己的脚丫亮出来实在不爽,中国古代女子的脚可是隐私部位,怎么可以亮出来给鞋匠看呢?所以我亮出来一个接口,我的脚是什么尺寸的,你按照尺寸给我做,其他脚的任何细节我不给你看,你休想轻薄我的脚。这样本来也没有什么不好。

但是某一天我要参加长跑,所以我对鞋提出了新的要求,要求比较有弹性,比较软,对于公共构造函数来说,简单,把你现在的鞋扔了,我给你重新做,你的脚是不能够改变的。对于静态工厂来说,别扔鞋阿,我做了多可惜,我们来把脚修一修,不要改变脚的外形,那样鞋就穿不下了,我们给脚动手术,增大脚弓,来提高跑动时候的弹跳力和持久力。

所以你为了保证接口契约的一致性付出了过高的代价,你要清楚的是,在实际的工程项目中,组装代码(或者说鞋)都不是代价很高的部分,都是可以快速生成,快速改变,快速升级,快速抛弃的部分。而业务逻辑对象即使你改变的不是外在的接口,而只是内在的行为,这种代价的高昂也会压垮你的项目。

所以就像你用劝告的语气告诉大家不要沉迷于fancy words,我也劝告你不要沉迷于单纯追求语义规则上的缜密和数学美感,而应该在实际的上规模的工程项目中体验一下两者的不同效果。
0 请登录后投票
   发表时间:2004-08-20  
引用
当出现相应的外部需求改变的时候

这是你的前提。我从来没有说外部需求改变的时候一定要通过改变静态函数内部的实现来达到目的。这种对新需求的应对,自然是要分析需求,找到最合适的方法。我的原文无意对此提出任何解决方案。如果你能证明用了静态工厂就对应对新需求造成影响,那么可以解释一下。否则,这不是我要说的意思。

而请注意我的前提:“重构”。
我说的是:
重构时, 我本身就已经是需要改动X的实现。那么这种改动怎样能不波及到客户?

再重申一遍, 不是因为外部需求变化,而是因为重构的原因而想修改类X的实现。修改X在这个问题中就是需求,是问题本身。

看来我们还是对问题的前提的理解都没有统一。 你一枪,我一枪,都扎在了空气中。哎。交流真难啊。
0 请登录后投票
   发表时间:2004-08-20  
实际上这是一个工程设计的问题.工程开始和学术研究是不同的,这一点大家都清楚.工程最重要的是考虑经济的问题,也就是任何手段都是以其经济效益为选择标准的最重要部分.
我喜欢动态的结构,但是动态结构往往会带来开发过程的复杂和维护的复杂.而如果你把所有的东西都想做到动态的时候,配置本身就成为一种复杂的编程而不是一种设置.所以首先我们就应该权衡一下究竟什么需要我们去做动态的调整.也就是说如果你几乎不会去跑步,那么就不要去考虑将来是不是要穿跑鞋的问题.这一点我绝对是以XP的法则为指导思想的.程序员往往都喜欢复杂的解决,而简单的方式让他们觉得泄气.这往往会带来很多的不必要的麻烦.
其实很多时候使用静态工厂并不是大逆不道,即使需要在将来修改,你一样可以动用重构这个工具.
就如同分层,我们知道分层原则上说会带来结构的清晰,但是这绝对是要有代价的.你使用MVC一样会有代价,我就喜欢用VB这样的东西,简单而简单,只是设置设置属性,只是写最少的代码,就可以有一个可以工作的程序.虽然这样子好像很业余,但是这个世界上大多数软件都是这样开发出来的.而当需求复杂起来或者结构中的臭味让我不能再忍受的时候,我会去做重构,把层次划分出来.就如同我会在我觉得必要的时候把静态工厂修改为动态的构造函数一样,我不觉得这个问题是一个大麻烦.
首先我追求的就是可以工作的程序,结构的优雅只能在这个基础之上才有意义.所以ajoo其实也是在沉迷于fancy words.
我这个人比较笨,年纪这么大也没有学聪明,所以我只能坚持先可以工作,然后再说别的.但是我知道至少这个世界上和我一样笨的人还有很多,我们只能采取先工作,再重构的这个笨办法.并且我也知道很多人和我一样不喜欢写code,因此我们都尽量避免复杂实现.而如今软件的复杂以及超出我们的想象,我自然不想再添加新的复杂在其中.
落实到具体的实现来说,程序的底层工具,也就是我们使用的jdo/Hibernate/spring这些东西我们很少会去动.注意我说的是很少,而不是绝对,有些时候做针对性的定制是必须的.但是这样的机会少而又少.而业务框架实现的时候也应该尽量的稳定,需要修改的只是业务中的逻辑部分.比如你的客户这个类应该尽量保持接口的稳定,而修改的只是客户订货的方法逻辑这些部分.而这些逻辑几乎一定会被修改,将其作为一种动态的可以在发布以后可以动态修改的部分是绝对必要的.而每一个客户都有购买物品的实际时间,这个部分不管你如何修改只是将其修改为不同的显示方式,而这样一来修改的可能性就小得多(你只有有限的几种选择,再加上时区的因素),这个部分你做成为动态的设置就没有多大的必要,你只是需要做有些的设置参数传递就可以了,你完全没有必要去设计一个将来可以使用火星或者木星以至于其他别的星球的时间概念的复杂的动态实现.
不管怎么说静态工厂和动态的构造函数都是可以工作的实现,我们所要做的就是权衡.
0 请登录后投票
   发表时间:2004-08-20  
不是静态工厂有什么问题,也不是静态工厂就不能用,也不是说公共构造函数就非用不可,关键的分歧在于对象本身是否应该控制对象创建的权利和行为:

你的主张是对象内部要控制对象创建的权利和行为,因为对象不应该信任外部调用,只有控制了才安全。所以你把构造函数设为private,不给外部组装代码以new的可能性,只允许他通过你定制的静态工厂来获得一个对象本身,换句话说你只允许外部对象以你设定的方式来使用对象,也就是说你给对象的创建提出了一个潜在的前提假设,只有满足了这个前提假设,才允许创建对象

你不信任外部组装代码,你限定了外部创建对象的行为,这样的好处就是不会有潜在的恶意代码调用带来了副作用,坏处就是由于你的定制行为给对象创建设定了前提假设,而这种潜在的前提假设一旦在某个外在环境中无法得到满足时,你就无法创建对象了,此时你必须修改对象的内部结构,也就是说你要修改你的前提假设了。这就意味着在你的对象设计中,你是通过修改对象创建的前提假设条件的方式来适应外部环境的变化的,这意味着你的对象内部是不稳定的。


而我的主张是对象本身要不控制对象创建的权利和行为,要给外部组装者以他想要的任意方式来创建对象,我信任外部组装代码,我不限定外部创建对象的行为,我给他们以创建对象的任何自由。也就是说我不给对象创建设定任何前提假设,所以对象内部是稳定的,在任何外部环境变化的情况下,外部组装代码都可以他相应的外部条件相配合的创建方式来使用对象

所以何者合理,何者不合理,我想有过实际工程项目经验的人心里都是雪亮的。

总结一下,双方的分歧在于:

对象本身是否应该控制对象被创建的权利和行为,你的观点是应该控制,保证对象可以预期到任何情况下被调用之后的结果,这样没有副作用,安全。我的观点是放弃控制,给别人自由,我不需要预期也不关心对象被调用的结果。

引用
看来我们还是对问题的前提的理解都没有统一。 你一枪,我一枪,都扎在了空气中。哎。交流真难啊。


其实你从来都避重就轻,从来没有正面回答potian的帖子,也从来没有正面回答我的帖子,而是抓住帖子中的某一个细节用词,就孜孜不倦的把讨论引向对你有优势,以你为主的方面。因此除非你开始正面回答potian和我的问题,否则这场讨论始终是没有结果的,这不是potian在回避,也不是我在回避,我一直很积极努力的正面和你讨论,为了能够得到把问题讨论清楚,你可以看到我从来不提容器,抽象工程,配置文件这些枝枝叶叶的话题,而是始终紧扣我们分歧的核心点,究竟该不该控制对象创建,反到是你喜欢顾左右而言他,喜欢把话题岔开,不去直面本质问题。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics